package br.iars.model.recommendation.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.apache.mahout.cf.taste.common.NoSuchUserException;
import org.apache.mahout.cf.taste.common.Refreshable;
import org.apache.mahout.cf.taste.common.TasteException;
import org.apache.mahout.cf.taste.impl.common.FastIDSet;
import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator;
import org.apache.mahout.cf.taste.impl.model.GenericItemPreferenceArray;
import org.apache.mahout.cf.taste.impl.model.GenericUserPreferenceArray;
import org.apache.mahout.cf.taste.impl.neighborhood.NearestNUserNeighborhood;
import org.apache.mahout.cf.taste.impl.recommender.GenericUserBasedRecommender;
import org.apache.mahout.cf.taste.impl.similarity.AveragingPreferenceInferrer;
import org.apache.mahout.cf.taste.impl.similarity.PearsonCorrelationSimilarity;
import org.apache.mahout.cf.taste.model.DataModel;
import org.apache.mahout.cf.taste.model.Preference;
import org.apache.mahout.cf.taste.model.PreferenceArray;
import org.apache.mahout.cf.taste.neighborhood.UserNeighborhood;
import org.apache.mahout.cf.taste.recommender.RecommendedItem;
import org.apache.mahout.cf.taste.recommender.Recommender;
import org.apache.mahout.cf.taste.similarity.UserSimilarity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import br.iars.model.entity.Context;
import br.iars.model.entity.Item;
import br.iars.model.entity.User;
import br.iars.model.exception.PersistenceException;
import br.iars.model.recommendation.RecommendationDataModel;
import br.iars.model.recommendation.engine.JPADataModel;
import br.iars.model.recommendation.util.LongPrimitiveListIterator;

@Stateless
@SuppressWarnings("unchecked")
@TransactionManagement(TransactionManagementType.CONTAINER)
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class RecommendationDataModelImpl implements RecommendationDataModel, JPADataModel {
	private static final Logger log = LoggerFactory.getLogger(RecommendationDataModelImpl.class);

	@PersistenceContext(unitName = "recommendation-system-pu")
	private EntityManager entityManager;

	private Context context;
	private boolean isOnlyOwnContext;

	private Recommender recommender;

	private Map<Long, Map<Long, Float>> mapUserItemRate;
	private Map<Long, Long> mapItemContext;
	private Map<Long, Long> mapContextTotalItem;

	public RecommendationDataModelImpl() {
		super();
		mapUserItemRate = new HashMap<Long, Map<Long, Float>>();
		mapItemContext = new HashMap<Long, Long>();
		mapContextTotalItem = new HashMap<Long, Long>();
	}
	
	public void init() throws PersistenceException {
		try {
			Query query = entityManager.createQuery("FROM Preference p");
			List<br.iars.model.entity.Preference> preferencesList = (List<br.iars.model.entity.Preference>) query.getResultList();

			Map<Long, Float> preferenceUser = null;
			Long countItem = null;
			log.info("Reading " + preferencesList.size() + " preferences");
			for (br.iars.model.entity.Preference p : preferencesList) {
				// Load mapUserItemRate
				preferenceUser = mapUserItemRate.get(p.getUserID());
				if (preferenceUser == null)
					preferenceUser = new HashMap<Long, Float>();

				preferenceUser.put(p.getItemID(), p.getRate());
				mapUserItemRate.put(p.getUserID(), preferenceUser);

				// Load mapContextItem
				countItem = mapContextTotalItem.get(p.getItem().getContext().getId());
				if (countItem == null)
					countItem = 0L;

				if (mapItemContext.get(p.getItemID()) == null)
					mapContextTotalItem.put(p.getItem().getContext().getId(), ++countItem);

				// Load mapItemContext
				mapItemContext.put(p.getItemID(), p.getItem().getContext().getId());
			}
			log.info("Number of users: " + mapUserItemRate.size());
			log.info("Number of itens: " + mapItemContext.size());
			log.info("Number of contexts: " + mapContextTotalItem.size());

		} catch (Exception e) {
			throw new PersistenceException("", e);
		}
	}

	public Recommender getRecommenderInstance() throws PersistenceException {
		try {
			if (recommender == null) {
				DataModel model = this;
				UserSimilarity userSimilarity = new PearsonCorrelationSimilarity(model);
				userSimilarity.setPreferenceInferrer(new AveragingPreferenceInferrer(model));
				UserNeighborhood neighborhood = new NearestNUserNeighborhood(3, userSimilarity, model);
				recommender = new GenericUserBasedRecommender(model, neighborhood, userSimilarity);
			}
			return recommender;
		} catch (TasteException e) {
			throw new PersistenceException("", e);
		}
	}

	public void setPreference(br.iars.model.entity.Preference preference) throws PersistenceException {
		try {
			StringBuilder jpql = new StringBuilder();
			jpql.append("FROM Preference p WHERE p.item.id = :itemId AND p.user.id = :userId");

			Query query = entityManager.createQuery(jpql.toString());
			query.setParameter("itemId", preference.getItemID());
			query.setParameter("userId", preference.getUserID());

			br.iars.model.entity.Preference p;
			try {
				p = (br.iars.model.entity.Preference) query.getSingleResult();
			} catch (NoResultException e) {
				p = null;
			}

			if (p != null) {
				p.setRate(preference.getRate());
			} else {
				p = preference;
			}

			entityManager.merge(p);
		} catch (Exception e) {
			throw new PersistenceException("", e);
		}
	}

	public List<br.iars.model.entity.Preference> recommend(Context context, User user, Integer quantity, Boolean isOnlyOwnContext)
			throws PersistenceException {
		this.context = context;
		this.isOnlyOwnContext = isOnlyOwnContext;
		List<br.iars.model.entity.Preference> preferences = new ArrayList<br.iars.model.entity.Preference>();

		try {
			init();
			
			List<RecommendedItem> recommendedItens = getRecommenderInstance().recommend(user.getId(), quantity);

			if (recommendedItens != null) {
				br.iars.model.entity.Preference p = null;

				for (RecommendedItem item : recommendedItens) {
					p = new br.iars.model.entity.Preference();
					p.setItem(new Item(item.getItemID()));
					p.getItem().setContext(context);
					p.setUser(user);
					p.setRate(item.getValue());

					preferences.add(p);
				}
			}
		} catch (TasteException e) {
			throw new PersistenceException("", e);
		}

		return preferences;
	}

	public Float estimatePreference(Context context, User user, Item item, Boolean isOnlyOwnContext) throws PersistenceException {
		this.context = context;
		this.isOnlyOwnContext = isOnlyOwnContext;
		try {
			init();
			return getRecommenderInstance().estimatePreference(user.getId(), item.getId());
		} catch (TasteException e) {
			throw new PersistenceException("", e);
		}
	}

	public LongPrimitiveIterator getUserIDs() throws TasteException {
		try {
			List<Long> userIDS = new LinkedList<Long>();

			Set<Long> users = new TreeSet<Long>(mapUserItemRate.keySet());

			for (Long user : users) {
				for (Long item : mapUserItemRate.get(user).keySet()) {
					if (mapItemContext.get(item).equals(context.getId())) {
						userIDS.add(user);
						break;
					}
				}
			}

			return new LongPrimitiveListIterator(userIDS);
		} catch (Exception e) {
			throw new TasteException("", e);
		}
	}

	public PreferenceArray getPreferencesFromUser(long userID) throws TasteException {

		try {
			List<Preference> userPreferences = new LinkedList<Preference>();

			if (mapUserItemRate.get(userID) == null)
				throw new NoSuchUserException();

			Float rate = null;
			Preference p = null;
			Set<Long> itens = new TreeSet<Long>(mapUserItemRate.get(userID).keySet());
			for (Long item : itens) {
				if (isOnlyOwnContext) {
					if (!context.getId().equals(mapItemContext.get(item)))
						continue;
				}
				rate = mapUserItemRate.get(userID).get(item);
				p = new br.iars.model.entity.Preference(new User(userID), new Item(item), rate);
				userPreferences.add(p);
			}

			return new GenericUserPreferenceArray(userPreferences);
		} catch (Exception e) {
			throw new TasteException("", e);
		}

	}

	public FastIDSet getItemIDsFromUser(long userID) throws TasteException {

		try {
			long[] itemIDS = null;

			if (mapUserItemRate.get(userID) == null)
				throw new NoSuchUserException();

			int i = 0;
			itemIDS = new long[mapUserItemRate.get(userID).keySet().size()];

			Set<Long> itens = new TreeSet<Long>(mapUserItemRate.get(userID).keySet());
			for (Long item : itens) {
				if (mapItemContext.get(item).equals(context.getId())) {
					itemIDS[i++] = item;
				}
			}

			if (i == 0)
				throw new NoSuchUserException();

			// Redimensiona o array
			itemIDS = Arrays.copyOf(itemIDS, i);

			FastIDSet result = new FastIDSet();
			result.addAll(itemIDS);
			return result;
		} catch (Exception e) {
			throw new TasteException("", e);
		}

	}

	public LongPrimitiveIterator getItemIDs() throws TasteException {
		try {
			List<Long> itemIDS = new LinkedList<Long>();

			Set<Long> itens = new TreeSet<Long>(mapItemContext.keySet());
			for (Long itemId : itens) {
				if (mapItemContext.get(itemId).equals(context.getId()))
					itemIDS.add(itemId);
			}

			return new LongPrimitiveListIterator(itemIDS);
		} catch (Exception e) {
			throw new TasteException("", e);
		}
	}

	public PreferenceArray getPreferencesForItem(long itemID) throws TasteException {

		try {
			Float rate = null;
			br.iars.model.entity.Preference p = null;
			List<Preference> preferences = new LinkedList<Preference>();

			Set<Long> users = new TreeSet<Long>(mapUserItemRate.keySet());

			for (Long user : users) {
				if ((rate = mapUserItemRate.get(user).get(itemID)) != null) {
					p = new br.iars.model.entity.Preference(new User(user), new Item(itemID), rate);
					preferences.add(p);
				}
			}

			return new GenericItemPreferenceArray(preferences);
		} catch (Exception e) {
			throw new TasteException("", e);
		}

	}

	public Float getPreferenceValue(long userID, long itemID) throws TasteException {

		try {
			Map<Long, Float> itens = mapUserItemRate.get(userID);
			if (itens == null) {
				return null;
			}
			return itens.get(itemID);

		} catch (Exception e) {
			throw new TasteException("", e);
		}
	}

	public int getNumItems() throws TasteException {
		try {
			return mapContextTotalItem.get(context.getId()).intValue();
		} catch (Exception e) {
			throw new TasteException("", e);
		}
	}

	public int getNumUsers() throws TasteException {
		try {
			return mapUserItemRate.size();
		} catch (Exception e) {
			throw new TasteException("", e);
		}
	}

	public int getNumUsersWithPreferenceFor(long... itemIDs) throws TasteException {
		if (itemIDs == null) {
			throw new IllegalArgumentException("itemIDs is null");
		}

		try {

			int count = 0;
			boolean hasPreference;

			for (Long user : mapUserItemRate.keySet()) {
				hasPreference = true;

				for (Long itemID : itemIDs) {
					if (mapUserItemRate.get(user).get(itemID) == null) {
						hasPreference = false;
						break;
					}
				}

				if (hasPreference) {
					count++;
				}
			}

			return count;

		} catch (Exception e) {
			throw new TasteException("", e);
		}
	}

	public void setPreference(long userID, long itemID, float value) throws TasteException {

	}

	public void removePreference(long userID, long itemID) throws TasteException {
		try {
			StringBuilder jpql = new StringBuilder();
			jpql.append("DELETE FROM Preference p WHERE p.item.context.id = :contextId ");
			jpql.append("AND p.user.id = :userId AND p.item.id = :itemId");

			Query query = entityManager.createQuery(jpql.toString());
			query.setParameter("contextId", context.getId());
			query.setParameter("userId", userID);
			query.setParameter("itemId", itemID);

			if (query.executeUpdate() <= 0) {
				throw new TasteException("");
			}
		} catch (Exception e) {
			throw new TasteException("", e);
		}
	}

	public boolean hasPreferenceValues() {
		return true;
	}

	public void refresh(Collection<Refreshable> alreadyRefreshed) {

	}
}
